#!/usr/bin/python
#
# Urwid web (CGI/Asynchronous Javascript) display module
#    Copyright (C) 2004-2007  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: http://excess.org/urwid/

"""
Urwid web application display module - adapted to run within the
PyFlag framework.
"""
import os
import sys
import signal
import random
import glob

try:
	import urwid.util
except ImportError:
	pass


_js_code = r"""
// Urwid web (CGI/Asynchronous Javascript) display module
//    Copyright (C) 2004-2005  Ian Ward
//
//    This library is free software; you can redistribute it and/or
//    modify it under the terms of the GNU Lesser General Public
//    License as published by the Free Software Foundation; either
//    version 2.1 of the License, or (at your option) any later version.
//
//    This library is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//    Lesser General Public License for more details.
//
//    You should have received a copy of the GNU Lesser General Public
//    License along with this library; if not, write to the Free Software
//    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
// Urwid web site: http://excess.org/urwid/

colours = new Object();
colours = {
	'0': "black",
	'1': "#c00000",
	'2': "green",
	'3': "#804000",
	'4': "#0000c0",
	'5': "#c000c0",
	'6': "teal",
	'7': "silver",
	'8': "gray",
	'9': "#ff6060",
	'A': "lime",
	'B': "yellow",
	'C': "#8080ff",
	'D': "#ff40ff",
	'E': "aqua",
	'F': "white"
};

keycodes = new Object();
keycodes = {
	8: "backspace", 9: "tab", 13: "enter", 27: "esc",
	33: "page up", 34: "page down", 35: "end", 36: "home",
	37: "left", 38: "up", 39: "right", 40: "down",
	45: "insert", 46: "delete",
	112: "f1", 113: "f2", 114: "f3", 115: "f4", 
	116: "f5", 117: "f6", 118: "f7", 119: "f8", 
	120: "f9", 121: "f10", 122: "f11", 123: "f12"
	};

var conn = null;
var char_width = null;
var char_height = null;
var screen_x = null;
var screen_y = null;

var urwid_id = null;
var send_conn = null;
var send_queue_max = 32;
var send_queue = new Array(send_queue_max);
var send_queue_in = 0;
var send_queue_out = 0;

var check_font_delay = 1000;
var send_more_delay = 100;
var poll_again_delay = 50000;

var document_location = null;

var update_method = "polling";

var sending = false;
var lastkeydown = null;

function setup_connection() {
	if (window.XMLHttpRequest) {
		conn = new XMLHttpRequest();
	} else if (window.ActiveXObject) {
		conn = new ActiveXObject("Microsoft.XMLHTTP");
	}

	if (conn == null) {
		set_status("Connection Failed");
		alert( "Can't figure out how to send request." );
		return;
	}
	try{
		conn.multipart = false;
	}catch(e){
		update_method = "polling";
	}
	conn.onreadystatechange = handle_recv;
	conn.open("POST", document_location, true);
	conn.setRequestHeader("X-Urwid-Method",update_method);
	conn.setRequestHeader("Content-type","text/plain");
	conn.send("window resize " +screen_x+" "+screen_y+"\n");
}

function do_poll() {
	if (window.XMLHttpRequest) {
		conn = new XMLHttpRequest();
	} else if (window.ActiveXObject) {
		conn = new ActiveXObject("Microsoft.XMLHTTP");
	}
	conn.onreadystatechange = handle_recv;
	conn.open("POST", document_location, true);
	conn.setRequestHeader("X-Urwid-Method","polling");
	conn.setRequestHeader("Content-type","text/plain");
	conn.send("eh?\n");
}

function handle_recv() {
	if( ! conn ){ return;}
	if( conn.readyState != 4) {
		return;
	}
	if( conn.status == 404 && urwid_id != null) {
		set_status("Connection Closed");
		return;
	}
	if( conn.status == 403 && update_method == "polling" ) {
		set_status("Server Refused Connection");
		alert("This server does not allow polling clients.\n\n" +
			"Please use a web browser with multipart support " +
			"such as Mozilla Firefox");
		return;
	}
	if( conn.status == 503 ) {
		set_status("Connection Failed");
		alert("The server has reached its maximum number of "+
			"connections.\n\nPlease try again later.");
		return;
	}
	if( conn.status != 200) {
		set_status("Connection Failed");
		alert("Error from server: "+conn.statusText);
		return;
	}
	if( send_queue_in != send_queue_out ){
	   // keys waiting
	   do_send(); 
	}

	set_status("Connected");
	
	if( conn.responseText == "" ){
		if(update_method=="polling"){
			poll_again();
		}
		return; // keepalive
	}
	if( conn.responseText == "Z" ){
		set_status("Connection Closed");
		update_method = null;
		return;
	}

	process_connection(conn);
	
	if(update_method=="polling"){
		poll_again();
	}
}

function process_connection(conn) {
	var text = document.getElementById('text');
	var last_screen = Array(text.childNodes.length);
	for( var i=0; i<text.childNodes.length; i++ ){
		last_screen[i] = text.childNodes[i];
	}
	
	var frags = conn.responseText.split("\n");
	var ln = document.createElement('span');
	var k = 0;
	for( var i=0; i<frags.length; i++ ){
		var f = frags[i];
		if( f == "" ){
			var br = document.getElementById('br').cloneNode(true);
			ln.appendChild( br );
			if( text.childNodes.length > k ){
	                        text.replaceChild(ln, text.childNodes[k]);
			}else{
				text.appendChild(ln);
			}
			k = k+1;
			ln = document.createElement('span');
		}else if( f.charAt(0) == "<" ){
			line_number = parseInt(f.substr(1));
			if( line_number == k ){
				k = k +1;
				continue;
			}
			var clone = last_screen[line_number].cloneNode(true);
			if( text.childNodes.length > k ){
				text.replaceChild(clone, text.childNodes[k]);
			}else{
				text.appendChild(clone);
			}
			k = k+1;
		}else{
			var span=make_span(f.substr(2),f.charAt(0),f.charAt(1));
			ln.appendChild( span );
		}
	}
	for( var i=k; i < text.childNodes.length; i++ ){
		text.removeChild(last_screen[i]);
	}
}

function poll_again(){
	if(conn.status == 200){
		setTimeout("do_poll();",poll_again_delay);
	}
}


function load_web_display(){
	if( document.documentURI ){
		document_location = document.documentURI;
	}else{
		document_location = document.location;
	}

	document.onkeypress = body_keypress;
	document.onkeydown = body_keydown;
	document.onresize = body_resize;
	document.onclick = mouse_click;
	
	body_resize();
	send_queue_out = send_queue_in; // don't queue the first resize

	set_status("Connecting");
	setup_connection();
	
	setTimeout("check_fontsize();",check_font_delay);
}

function set_status( status ){
	var s = document.getElementById('status');
	var t = document.createTextNode(status);
	if(s) s.replaceChild(t, s.firstChild);
}

function make_span(s, fg, bg){
	d = document.createElement('span');
	d.style.backgroundColor = colours[bg];
	d.style.color = colours[fg];
	d.appendChild(document.createTextNode(s));
	
	return d;
}

function mouse_click(e) {
   var posx = 0;
   var posy = 0;
   if (!e) var e = window.event;

   text = document.getElementById('urwid_text');
   var r= getAbsolutePosition(text);
   
   if (e.pageX || e.pageY) {
       posx = e.pageX;
       posy = e.pageY;
   }
   
    else if (e.clientX || e.clientY) {
        posx = e.clientX + text.scrollLeft + document.documentElement.scrollLeft;
	posy = e.clientY + text.scrollTop + document.documentElement.scrollTop;
    };

    posx = Math.floor((posx - r.x)/char_width);
    posy = Math.floor((posy - r.y)/char_height);
    
    send_key("mouse_press 1 "+posx+" "+posy);
    stop_key_event(e);
};

function body_keydown(e){
	if (conn == null){
		return;
	}
	if (!e) var e = window.event;
	if (e.keyCode) code = e.keyCode;
	else if (e.which) code = e.which;

	var mod = "";
	var key;

	if( e.ctrlKey ){ mod = "ctrl " + mod; }
	if( e.altKey || e.metaKey ){ mod = "meta " + mod; }
	if( e.shiftKey && e.charCode == 0 ){ mod = "shift " + mod; }

	key = keycodes[code];
	
	if( key != undefined ){
		lastkeydown = key;
		send_key( mod + key );
		stop_key_event(e);
		return false;
	}
}

function body_keypress(e){
	if (conn == null){
		return;
	}

	if (!e) var e = window.event;
	if (e.keyCode) code = e.keyCode;
	else if (e.which) code = e.which;

	var mod = "";
	var key;

	if( e.ctrlKey ){ mod = "ctrl " + mod; }
	if( e.altKey || e.metaKey ){ mod = "meta " + mod; }
	if( e.shiftKey && e.charCode == 0 ){ mod = "shift " + mod; }
	
	if( e.charCode != null && e.charCode != 0 ){
		key = String.fromCharCode(e.charCode);
	}else if( e.charCode == null ){
		key = String.fromCharCode(code);
	}else{
		key = keycodes[code];
		if( key == undefined || lastkeydown == key ){
			lastkeydown = null;
			stop_key_event(e);
			return false;
		}
	}
	
	send_key( mod + key );
	stop_key_event(e);
	return false;
}

function stop_key_event(e){
	e.cancelBubble = true;
	if( e.stopPropagation ){
		e.stopPropagation();
	}
	if( e.preventDefault  ){
		e.preventDefault();
	}
}

function send_key( key ){
	if( (send_queue_in+1)%send_queue_max == send_queue_out ){
		// buffer overrun
		return;
	}
	send_queue[send_queue_in] = key;
	send_queue_in = (send_queue_in+1)%send_queue_max;

	if (send_conn == undefined || send_conn.ready_state != 4 ){
	   send_more();
	   return;
	}
	do_send();
}

function do_send() {
	if( ! update_method ){ return; } // connection closed
	if( send_queue_in == send_queue_out ){ return; }
	if( sending ){ 
		//var queue_delta = send_queue_in - send_queue_out;
		//if( queue_delta < 0 ){ queue_delta += send_queue_max; }
		//set_status("Sending (queued "+queue_delta+")"); 
		return; 
	}
	try{
		sending = true;
		//set_status("starting send");
		if( send_conn == null ){
			if (window.XMLHttpRequest) {
				send_conn = new XMLHttpRequest();
			} else if (window.ActiveXObject) {
				send_conn = new ActiveXObject("Microsoft.XMLHTTP");
			}
		}else if( send_conn.status != 200) {
			alert("Error from server: "+send_conn.statusText);
			return;
		}else if(send_conn.readyState != 4 ){
			alert("not ready on send connection");
			return;
		}
	} catch(e) {
		alert(e);
		sending = false;
		return;
	}
	send_conn.open("POST", document_location + "&input=1", true);
	send_conn.onreadystatechange = send_handle_recv;
	send_conn.setRequestHeader("Content-type","text/plain");
	send_conn.setRequestHeader("X-Urwid-ID",urwid_id);
	var tmp_send_queue_in = send_queue_in;
	var out = null;
	if( send_queue_out > tmp_send_queue_in ){
		out = send_queue.slice(send_queue_out).join("\n")
		if( tmp_send_queue_in > 0 ){
			out += "\n"  + send_queue.slice(0,tmp_send_queue_in).join("\n");
		}
	}else{
		out = send_queue.slice(send_queue_out,
		     tmp_send_queue_in).join("\n");
	}
	send_queue_out = tmp_send_queue_in;
	//set_status("Sending");
	send_conn.send( out +"\n" );
}

function send_handle_recv() {
	if( send_conn.readyState != 4) {
		return;
	}
	if( send_conn.status == 404) {
		set_status("Connection Closed");
		update_method = null;
		return;
	}
	if( send_conn.status != 200) {
		alert("Error from server: "+send_conn.statusText);
		return;
	}
	
	sending = false;
	process_connection(send_conn);

	
	if( send_queue_out != send_queue_in ){
		send_more();
	}
}

function send_more(){
	setTimeout("do_send();",send_more_delay);
}

function check_fontsize(){
	body_resize()
	setTimeout("check_fontsize();",check_font_delay);
}

function body_resize(){
	var t = document.getElementById('testchar');
	var t2 = document.getElementById('testchar2');
	var text = document.getElementById('text');

	var window_width;
	var window_height;
	if (window.innerHeight) {
		window_width = window.innerWidth;
		window_height = window.innerHeight;
	}else{
		window_width = document.documentElement.clientWidth;
		window_height = document.documentElement.clientHeight;
		//var z = "CI:"; for(var i in bod){z = z + " " + i;} alert(z);
	}

	char_width = t.offsetLeft / 44;
	var avail_width = window_width-18;
	var avail_width_mod = avail_width % char_width;
	var x_size = (avail_width - avail_width_mod)/char_width;
	
	char_height = t2.offsetTop - t.offsetTop;
	var avail_height = window_height-text.offsetTop-10;
	var avail_height_mod = avail_height % char_height;
	var y_size = (avail_height - avail_height_mod)/char_height;
	
	text.style.width = x_size*char_width+"px";
	text.style.height = y_size*char_height+"px";

	if( screen_x != x_size || screen_y != y_size ){
		send_key("window resize "+x_size+" "+y_size);
	}
	screen_x = x_size;
	screen_y = y_size;
}

"""

ALARM_DELAY = 60
POLL_CONNECT = 3
MAX_COLS = 200
MAX_ROWS = 100
MAX_READ = 4096
BUF_SZ = 16384

_code_colours = {
	'black':		"0",
	'dark red':		"1",
	'dark green':		"2",
	'brown':		"3",
	'dark blue':		"4",
	'dark magenta':		"5",
	'dark cyan':		"6",
	'light gray':		"7",
	'dark gray':		"8",
	'light red':		"9",
	'light green':		"A",
	'yellow':		"B",
	'light blue':		"C",
	'light magenta':	"D",
	'light cyan':		"E",
	'white':		"F",
}

# replace control characters with ?'s
_trans_table = "?" * 32 + "".join([chr(x) for x in range(32, 256)])

_css_style = """
body {	margin: 8px 8px 8px 8px; border: 0; 
	color: black; background-color: silver;
	font-family: fixed; overflow: hidden; }

form { margin: 0 0 8px 0; }

#text { position: relative;
	background-color: silver;
	width: 100%; height: 100%;
	margin: 3px 0 0 0; border: 1px solid #999; }

#page { position: relative;  width: 100%;height: 100%;}
"""

# HTML Initial Page
_html_page = [
"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
 "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<style type="text/css">
""" + _css_style + """
</style>
</head>
<body id="body" onload="load_web_display()">
<div style="position:absolute; visibility:hidden;">
<br id="br"\> 
<pre>The quick brown fox jumps over the lazy dog.<span id="testchar">X</span>
<span id="testchar2">Y</span></pre>
</div>
<script type="text/javascript">
//<![CDATA[
""" + _js_code +"""
//]]>
</script>
<script src="javascript/functions.js"></script>
<div id="urwid_text">
<pre id="text"></pre>
</div>
</body>
</html>
"""]

class Screen:
	def __init__(self):
		self.palette = {}
		self.has_color = True
		## This is the updated version of the current screen
		## buffer. self.draw_screen() will fill this with the
		## most recent version of the screen - so our called
		## just grabs this.
		self.buffer = ''

		## Lazy initialization
		self.initialised = False
	
	def register_palette( self, l ):
		"""Register a list of palette entries.

		l -- list of (name, foreground, background) or
		     (name, same_as_other_name) palette entries.

		calls self.register_palette_entry for each item in l
		"""
		
		for item in l:
			if len(item) in (3,4):
				self.register_palette_entry( *item )
				continue
			assert len(item) == 2, "Invalid register_palette usage"
			name, like_name = item
			if not self.palette.has_key(like_name):
				raise Exception("palette entry '%s' doesn't exist"%like_name)
			self.palette[name] = self.palette[like_name]

	def register_palette_entry( self, name, foreground, background, 
		mono=None):
		"""Register a single palette entry.

		name -- new entry/attribute name
		foreground -- foreground colour
		background -- background colour
		mono -- monochrome terminal attribute

		See curses_display.register_palette_entry for more info.
		"""
		if foreground == "default":
			foreground = "black"
		if background == "default":
			background = "light gray"
		self.palette[name] = (foreground, background, mono)

	def set_mouse_tracking(self):
		"""Not yet implemented"""
		pass

	def start(self):
		"""	
		This function reads the initial screen size, generates a
		unique id and handles cleanup when fn exits.
		
		web_display.set_preferences(..) must be called before calling
		this function for the preferences to take effect
		"""
		try:
			assert self.buffer.startswith("window resize "),self.buffer
			ignore1,ignore2,x,y = self.buffer.split(" ",3)
		except AssertionError:
			x,y = 120,25

		x = int(x)
		y = int(y)
		self._set_screen_size( x, y )
		self.last_screen = {}
		self.last_screen_width = 0
	
		self.input_tail = ""
		self.buffer = ''
		self.initialised = True

	def run_wrapper(self,fn):
		"""
		Run the application main loop, calling start() first
		and stop() on exit.
		"""
		if not self.initialised:
			self.start()
			
		while 1:
			data = (yield None)
			fn().send(data)

	def _set_screen_size(self, cols, rows ):
		"""Set the screen size (within max size)."""
		
		if cols > MAX_COLS:
			cols = MAX_COLS
		if rows > MAX_ROWS:
			rows = MAX_ROWS
		self.screen_size = cols, rows
		
	def draw_screen(self, (cols, rows), r ):
		"""Send a screen update to the client."""
		if cols != self.last_screen_width:
			self.last_screen = {}

		self.buffer = ''

		assert r.rows() == rows
	
		if r.cursor is not None:
			cx, cy = r.cursor
		else:
			cx = cy = 0

		new_screen = {}
		
		y = -1
		for row in r.content():
			y += 1
			row = list(row)
			
			l = []
			
			sig = tuple(row)
			if y == cy: sig = sig + (cx,)
			new_screen[sig] = new_screen.get(sig,[]) + [y]
			old_line_numbers = self.last_screen.get(sig, None)
			if old_line_numbers is not None:
				if y in old_line_numbers:
					old_line = y
				else:
					old_line = old_line_numbers[0]
				self.buffer +=  "<%d\n" % old_line
				continue
			
			col = 0
			for (a, cs, run) in row:
				run = run.translate(_trans_table)
				if a is None:
					fg,bg,mono = "black", "light gray", None
				else:
					fg,bg,mono = self.palette[a]
				if y == cy and col <= cx:
					run_width = urwid.util.calc_width(run, 0, 
						len(run))
					if col+run_width > cx:
						l.append(code_span(run, fg, bg,
							cx-col))
					else:
						l.append(code_span(run, fg, bg))
					col += run_width
				else:
					l.append(code_span(run, fg, bg))

			self.buffer += "".join(l)+"\n"
		self.last_screen = new_screen
		self.last_screen_width = cols

	def clear(self):
		"""
		Force the screen to be completely repainted on the next
		call to draw_screen().

		(does nothing for web_display)
		"""
		pass

	def get_cols_rows(self):
		"""Return the screen size."""
		return self.screen_size

	def get_input(self, data, raw_keys=False):
		"""Return pending input as a list."""
		l = []
		resized = False
		
		if not data:
			if raw_keys:
				return [],[]
			return []

		## Grab the keys from the input buffer
		keys = data.split("\n")
		keys[0] = self.input_tail + keys[0]
		self.input_tail = keys[-1]
		
		for k in keys[:-1]:
			if k.startswith("window resize "):
				ign1,ign2,x,y = k.split(" ",3)
				x = int(x)
				y = int(y)
				self._set_screen_size(x, y)
				resized = True
			elif k.startswith("mouse"):
				try:
					event,button, col, row = k.split()
					event = event.replace("_"," ")
					button = int(button)
					col = int(col)
					row = int(row)
					l.append((event, button, col, row))
				except:
					print "Unrecognised mouse command"
					continue
			else:
				l.append(k)
		if resized:
			l.append("window resize")
		
		if raw_keys:
			return l, []
		return l
	

def code_span( s, fg, bg, cursor = -1):
	code_fg = _code_colours[ fg ]
	code_bg = _code_colours[ bg ]
	
	if cursor >= 0:
		c_off, _ign = urwid.util.calc_text_pos(s, 0, len(s), cursor)
		c2_off = urwid.util.move_next_char(s, c_off, len(s))

		return ( code_fg + code_bg + s[:c_off] + "\n" +
			 code_bg + code_fg + s[c_off:c2_off] + "\n" +
			 code_fg + code_bg + s[c2_off:] + "\n")
	else:
		return code_fg + code_bg + s + "\n"


def html_escape(text):
	"""Escape text so that it will be displayed safely within HTML"""
	text = text.replace('&','&amp;')
	text = text.replace('<','&lt;')
	text = text.replace('>','&gt;')
	return text

def handle_short_request():
	"""
	Handle short requests such as passing keystrokes to the application
	or sending the initial html page.  If returns True, then this
	function recognised and handled a short request, and the calling
	script should immediately exit.

	web_display.set_preferences(..) should be called before calling this
	function for the preferences to take effect
	"""
	if os.environ['REQUEST_METHOD'] == "GET":
		# Initial request, send the HTML and javascript.
		sys.stdout.write("Content-type: text/html\r\n\r\n" +
			html_escape(_prefs.app_name).join(_html_page))
		return True
	
	if os.environ['REQUEST_METHOD'] != "POST":
		# Don't know what to do with head requests etc.
		return False
	
	if not os.environ.has_key('HTTP_X_URWID_ID'):
		# If no urwid id, then the application should be started.
		return False

	urwid_id = os.environ['HTTP_X_URWID_ID']
	if len(urwid_id)>20:
		#invalid. handle by ignoring
		#assert 0, "urwid id too long!"
		sys.stdout.write("Status: 414 URI Too Long\r\n\r\n")
		return True
	for c in urwid_id:
		if c not in "0123456789":
			# invald. handle by ignoring
			#assert 0, "invalid chars in id!"
			sys.stdout.write("Status: 403 Forbidden\r\n\r\n")
			return True
	
	if os.environ.get('HTTP_X_URWID_METHOD',None) == "polling":
		# this is a screen update request
		s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
		try:
			s.connect( os.path.join(_prefs.pipe_dir,
				"urwid"+urwid_id+".update") )
			data = "Content-type: text/plain\r\n\r\n"+s.recv(BUF_SZ)
			while data:
				sys.stdout.write(data)
				data = s.recv(BUF_SZ)
			return True
		except socket.error,e:
			sys.stdout.write("Status: 404 Not Found\r\n\r\n")
			return True
		
		
	
	# this is a keyboard input request
	try:
		fd = os.open((os.path.join(_prefs.pipe_dir,
			"urwid"+urwid_id+".in")), os.O_WRONLY)
	except OSError,e:
		sys.stdout.write("Status: 404 Not Found\r\n\r\n")
		return True
		
	keydata = sys.stdin.read(MAX_READ)
	os.write(fd,keydata)
	os.close(fd)
	sys.stdout.write("Content-type: text/plain\r\n\r\n")
	
	return True

